# pytombo - Python library for processing plain text + encrypted notes.
# Copyright (C) 2007  Chris Clark

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import re
import warnings

import tombonote
import chi_io


def safe_mkdir(newdir):
    """Create directory path(s) ignoring "already exists" errors"""
    result_dir = os.path.abspath(newdir)
    try:
        os.makedirs(result_dir)
    except OSError, info:
        if info.errno == 17 and os.path.isdir(result_dir):
            pass
        else:
            raise


class SearchException(Exception):
    def __init__(self, value):
        self.value = value
    
    def __str__(self):
        return repr(self.value)


class SearchCancelled(SearchException):
    def __init__(self, value):
        self.value = value
        
    def __str__(self):
        return repr(self.value)


config = {}


def set_config(config_obj):
    global config
    config = config_obj


# Filename filter functions

def is_text(in_filename):
    """filename filter, only allows (plain text) *.txt notes"""
    return in_filename.lower().endswith('.txt')


def is_encrypted(in_filename):
    """filename filter, only allows encrypted *.chi notes"""
    return in_filename.lower().endswith('.chi')


def note_filename_filter_gen(allow_encrypted=True, filename_filter_str=None):
    """filename_filter_str regex object
    """
    if allow_encrypted == 'only':
        # kinda hacky, only include encrypted (or filename matches)
        include_text = False
    else:
        include_text = True
    def temp_filter(in_filename):
        name = in_filename.lower()
        if filename_filter_str:
            if filename_filter_str.search(name):
                return True
            else:
                return False
        if include_text and name.endswith('.txt'):
            return True
        if allow_encrypted and name.endswith('.chi'):
            return True
        else:
            return False
    return temp_filter


any_filename_filter = lambda x: True  # allows any filename, i.e. no filtering


is_text_or_chi_note_filename_filter = note_filename_filter_gen(allow_encrypted=True)


def recurse_notes(path_to_search, filename_filter):
    """Walk directory of notes, directory depth first (just like Tombo find does), returns generator
    """
    ## Requires os.walk (python 2.3 and later).
    ## Pure Python versions for earlier versions available from:
    ##  http://osdir.com/ml/lang.jython.user/2006-04/msg00032.html
    ## but lacks "topdown" support, walk class later
    for dirpath, dirnames, filenames in os.walk(path_to_search, topdown=False):
        #print 'walker', repr((dirnames, filenames))
        filenames.sort()
        #print '\twalker', repr((dirnames, filenames))
        for temp_filename in filenames:
            if filename_filter(temp_filename):
                temp_filename = os.path.join(dirpath, temp_filename)
                #print 'yield ', temp_filename
                yield temp_filename


def fake_recurse_notes(path_to_search, filename_filter):
    """Same API as recurse_notes(), returns generator
        BUT used on a single file (and ignores filename_filter)
    """
    yield path_to_search


def directory_contents(dirname):
    """Simple non-recursive Tombo note lister.
    Returns tuple (list of directories, list of files) in @dirname"""
    ## TODO consider using 'dircache' instead of os.listdir?
    ## should not be re-reading so probably not a good idea
    file_list = []
    dir_list = []
    if os.path.isdir(dirname):
        for name in os.listdir(dirname):
            path = os.path.join(dirname, name)
            if os.path.isfile(path):
                if is_text_or_chi_note_filename_filter(name):
                    file_list.append(name)
            if os.path.isdir(path):
                dir_list.append(name)
    else:
        # assume a file, ignores s-links
        file_list.append(os.path.basename(dirname))

    dir_list.sort()
    file_list.sort()
    return dir_list, file_list

#####################
## From http://osdir.com/ml/lang.jython.user/2006-04/msg00032.html


class walk:
    '''walk() generates the file names in a directory tree, by walking the tree

    top down. For each directory in the tree rooted at
    directory top (including top itself), it yields a 3-tuple (dirpath,
    dirnames, filenames).

    This is os.walk() implemented with the old-style iterator protocol but ONLY implements "topdown=True", does NOT implement "onerror" at all.
    Currently unused :-(
    '''

    def __init__(self, dirpath):
        self.dirpath = dirpath
        self.filenames = []
        self.dirnames = []

        for f in os.listdir(dirpath):
            if os.path.isdir(os.path.join(dirpath, f)):
                self.dirnames.append(f)
            else:
                self.filenames.append(f)

        # Index into self.dirnames; -1 signals to yield current dir
        self.next = -1

    def __getitem__(self, ix):
        if self.next == -1:
            # Yield the current base dir
            self.next = 0
            return (self.dirpath, self.dirnames, self.filenames)

        try:
            # Return the next item from the current subdirectory
            return self.subwalk.__getitem__(None)
        except (AttributeError, IndexError):
            # No subdirectory started or current subdirectory exhausted
            # In either case, try to start the next
            self._next_sub()
            return self.subwalk.__getitem__(None)

    def _next_sub(self):
        ''' Start walking the next sub-directory or raise IndexError '''
        if self.next < len(self.dirnames):
            self.subwalk = walk(os.path.join(self.dirpath, self.dirnames[self.next]))
            self.next += 1
        else:
            # Nothing left to do
            raise IndexError
#####################


def grep_string(search_text, regex_object):
    """Given input string "search_text" and compiled regex regex_object
    search for regex matches and return list of tuples of line number and text for that line
    """
    linecount = 0
    results = []
    for x in search_text.split('\n'):
        linecount += 1
        if regex_object.search(x):
            results.append((linecount, x))
    return results


def search_func(search_term, search_path, search_term_is_a_regex=True, ignore_case=True, search_encrypted=False, get_password_callback=None):
    """
    Three types of "grep" (i.e. not full text search) search to support:

        1)  direct search for this phrase -- FIXME PERFORMANCE for Python 2.5 with new string code, "in" is faster than regex for straight phrases...
        2)  regex search
        3)  twist on #1, any of (or all) words -- stretch goal...
    """
    if not search_term_is_a_regex:
        search_term = re.escape(search_term)
    if ignore_case:
        regex_object = re.compile(search_term, re.IGNORECASE)
    else:
        regex_object = re.compile(search_term)
    is_note_filename_filter = note_filename_filter_gen(allow_encrypted=search_encrypted)
    file_count = 0
    match_count = 0
    for x in recurse_notes(search_path, is_note_filename_filter):
        note_text = tombonote.file_contents(x, get_password_callback)
        search_res = grep_string(note_text, regex_object)
        if search_res:
            #print x, search_res
            for result_hit_line, result_hit_text in search_res:
                print '%s:%d:%s' % (x, result_hit_line, result_hit_text)
            match_count += 1
        file_count += 1
    print '%d files searched' % file_count 
    print '%d files match' % match_count 


def example_progess_callback(*args, **kwargs):
    print 'example_progess_callback:', args, kwargs


# FIXME make an object where this is the method, do the same for ftsearch
def search_iter(search_term, search_path, search_term_is_a_regex=True, ignore_case=True, search_encrypted=False, get_password_callback=None, progess_callback=None, findonly_filename=None, index_name=None):
    # TODO FIXME findonly_filename must be implemented in search_func
    # TODO FIXME findonly_filename needs to be tested in tombogrep.py
    """Search for `search_term`, returning a generator of:
        tuple of (note_filename, [list of matches, tuple of (line_number, whole_line])
        Example:
            ['somefile.txt', [(2, 'line two'),]]
    
    Three types of "grep" (i.e. not full text search) search to support:

        1)  direct search for this phrase
        2)  regex search
        3)  twist on #1, any of (or all) words -- stretch goal...
    
    index_name - is here only for api compatibility (with full text search). It could be ignored if provided but this is probably a mistake by the caller so raise a warning
    """
    if index_name:
        warnings.warn('regex/grep text search requested with an index parameter')

    if not isinstance(search_path, basestring) or not os.path.exists(search_path):
        raise SearchException('bad search path %s' % search_path)
    if not search_term_is_a_regex:
        search_term = re.escape(search_term)
    if ignore_case:
        regex_object = re.compile(search_term, re.IGNORECASE)
    else:
        regex_object = re.compile(search_term)
    filename_filter_str = None
    if findonly_filename:
        filename_filter_str = regex_object
    is_note_filename_filter = note_filename_filter_gen(allow_encrypted=search_encrypted, filename_filter_str=filename_filter_str)
    if os.path.isfile(search_path):
        recurse_notes_func = fake_recurse_notes
    else:
        recurse_notes_func = recurse_notes
    for x in recurse_notes_func(search_path, is_note_filename_filter):
        if progess_callback:
            progess_callback(filename=x)
        if filename_filter_str:
            yield (x, [(1, 'FILENAME SEARCH HIT\n')])
        include_contents = True
        include_contents = False
        ## TODO decide what to do with include_contents - default or make a parameter
        if not filename_filter_str or include_contents:
            try:
                note_text = tombonote.file_contents(x, get_password_callback, dos_newlines=True)
            except chi_io.BadPassword, info:
                raise SearchCancelled(str(info))
            search_res = grep_string(note_text, regex_object)
            if search_res:
                yield (x, search_res)


if __name__ == "__main__":
    sys.exit()
    
    ## Example

    path_to_search = "."
    search_term = 'and'

    try:
        for hit in search_iter(search_term, path_to_search, search_term_is_a_regex=False, ignore_case=True, search_encrypted=True, get_password_callback=caching_console_password_prompt):
            filename, hit_detail = hit
            filename = tombonote.remove_leading_path(path_to_search, filename)
            for result_hit_line, result_hit_text in hit_detail:
                print '%s:%d:%s' % (filename, result_hit_line, result_hit_text)
    except SearchCancelled, info:
        print 'search cancelled because of', info
